import json
import os
import re
import shutil
import time
from copy import deepcopy
from typing import Final

import allure
import pytest

from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement

from enums.EngineActIdEnum import EngineActIdEnum
from enums.ErrSkipMsgEnum import ErrSkipMsgEnum
from SeleniumHelper import SeleniumHelper

from faker import Faker


def tplTestFunc(self):
    titleIdx = self.__class__.runner.currentTitleIdx
    self.__class__.runner.currentTitleIdx += 1
    self.__class__.runner.runTitle(titleIdx)


def runPyTest(dir: str, files: list[str], skipPyTest: bool, genAllureReport: bool):
    reportDir = os.path.join(dir, 'report')
    allureReportDir = os.path.abspath(os.path.join(dir, 'allure-report'))
    if not skipPyTest:
        try:
            shutil.rmtree(reportDir)
        except:
            pass

        agvs = [os.path.join(dir, iFile) for iFile in files]
        agvs.extend(['-s', '-q', "--alluredir", reportDir])
        pytest.main(agvs)

    if genAllureReport:
        allure_cmd = f"allure generate {reportDir} --clean -o {allureReportDir}"  # 将报告转换成html格式文件的命令
        os.system(allure_cmd)

        print(f"\nallure-report dir : {allureReportDir}")

    for iFile in files:
        os.remove(os.path.join(dir, iFile))

    print('done.')

    # allure_cmd2 = f"allure open {allureReportDir}"  # 将报告转换成html格式文件的命令
    # os.system(allure_cmd2)


class SeleniumTestRunner:
    VERSION: Final[list[int]] = [0, 0, 1]
    DOC_TYPE: Final[str] = 'SeleniumTestRunner-Plan-DDT'

    __argsReg: Final = re.compile(r"{{ARGS:(.*?)}}")
    __varsReg: Final = re.compile(r"{{VARS:(.*?)}}")
    __fakerReg: Final = re.compile(r"{{FAKER\.(.*?):?((?<=:).*?)?}}")
    __keysReg: Final = re.compile(r"{{Keys\.(.*?)}}")

    def __init__(self):
        self.doc: dict = None
        self.initSteps: list[dict] = None
        self.titleLst: list[dict] = None
        # self.titleRange: list = []
        self.currentTitleIdx: int = 0
        self.file: str = None
        self.fake: Faker = None
        self.screenshotOption: dict[str, int] = None
        self.__seleniumHelper: SeleniumHelper = None
        self.__vars: dict[str, any] = dict()
        self.exitIfSetupErr: bool = False
        self.skipModuleReason: str = ''

    def _resetVars(self):
        self.__vars.clear()

    @staticmethod
    def loadAll(dir: str, skipPyTest: bool, genAllureReport: bool) -> None:
        files = os.listdir(dir)
        jsonFiles: list[str] = []
        plans: dict[str, dict] = {}
        with open('TplOfTestPy.pyTpl', 'r', encoding="utf-8") as f:
            tplStr: str = f.read()
        pyFiles: list[str] = []
        for iFileName in files:
            iFileName2 = os.path.join(dir, iFileName)
            if os.path.isfile(iFileName2):
                d = os.path.splitext(iFileName)
                if d[1].lower() == ".json":
                    absFile = os.path.abspath(iFileName2)
                    with open(absFile, 'r', encoding='utf-8') as fs:
                        doc: dict = json.load(fs)
                        if not SeleniumTestRunner.__checkDocVersion(doc):
                            continue

                    jsonFiles.append(absFile)
                    plans[d[0]] = doc
                    pyName = f'test_{d[0]}.py'
                    pyFiles.append(pyName)
                    with open(os.path.join(dir, pyName), 'w', encoding="utf-8") as f:
                        s = tplStr.replace('{{DOC_FILE}}', iFileName)
                        s = s.replace('{{DOC_PATH}}', absFile)
                        f.write(s)
        runPyTest(dir, pyFiles, skipPyTest, genAllureReport)
        return

    @staticmethod
    def loadFile(file: str):
        with open(file, 'r', encoding='utf-8') as fs:
            doc: dict = json.load(fs)
            if not SeleniumTestRunner.__checkDocVersion(doc):
                return False

        self = SeleniumTestRunner()
        self.doc = doc
        self.file = file
        titleLst = self.titleLst = []
        plan: dict = doc['plan']
        setupSteps: list = doc.get('init').get('setupSteps')
        if setupSteps:
            self.initSteps = []
            self.exitIfSetupErr = doc.get('init').get('exitIfSetupErr')
            for iStep in setupSteps:
                self.initSteps.append(iStep)

        features = plan['features']
        for iFeature in features:
            stories = iFeature['stories']
            for iStory in stories:
                titles: list[dict] = iStory['titles']
                for iTitle in titles:
                    iTitle['feature'] = iFeature.get('name')
                    iTitle['story'] = iStory.get('name')
                    repeat = iTitle.get('repeat', 1)
                    if repeat > 1:
                        titleName = iTitle['name']
                        iRepeat = 1
                        while iRepeat <= repeat:
                            iTitle['name'] = titleName + f' {iRepeat}/{repeat}'
                            titleLst.append(iTitle.copy())
                            iRepeat += 1
                    else:
                        titleLst.append(iTitle)
        # self.titleRange = [''] * len(titleLst)
        self.currentTitleIdx = 0
        return self

    @staticmethod
    def __checkDocVersion(doc: dict) -> bool:
        if doc.get('docType') != SeleniumTestRunner.DOC_TYPE:
            return False
        docLibVer: str = doc.get('engineVersion')
        if not docLibVer:
            return False
        docLibVerLst: list[str] = docLibVer.split('.')
        if len(docLibVerLst) != 3:
            return False
        verIdx = 0
        while verIdx < 3:
            if int(docLibVerLst[verIdx]) > SeleniumTestRunner.VERSION[verIdx]:
                return False
            verIdx += 1
        return True

    def initClass(self, cls: type):

        nextTestIdx: int = 0
        for iTitle in self.titleLst:
            setattr(cls, 'test_' + str(nextTestIdx).zfill(5), tplTestFunc)
            nextTestIdx += 1

    def initRunner(self):
        initDict: dict = self.doc['init']
        self.currentTitleIdx = 0
        with allure.step("--启动参数--"):
            with allure.step(f'设定浏览器类型: {initDict["browserType"]}'):
                browserType = initDict["browserType"]
            implicitly_wait = initDict.get('implicitly_wait')
            if implicitly_wait:
                with allure.step(f'设定隐性等待: {implicitly_wait}秒'):
                    assert True
            else:
                implicitly_wait = 10
                with allure.step(f'设定隐性等待为默认值: {implicitly_wait}秒'):
                    pass

            options: list[str] = initDict.get('browserOptions')
            if options and len(options) > 0:
                with allure.step(f'设定浏览器启动选项: {len(options)}项'):
                    for iOption in options:
                        with allure.step(iOption):
                            assert True

            pageLoadStrategy = initDict.get('pageLoadStrategy')

            fakerLocal: str = initDict.get('fakerLocal') or "zh_CN"
            with allure.step(f'设定Faker本地语言: {fakerLocal}'):
                self.fake = Faker(locale=fakerLocal)

            ssOption: dict = initDict.get('screenshot', dict({'before': 0, 'after': 2}))
            ssBefore: int = ssOption.get('before', 0)
            ssAfter: int = ssOption.get('after', 2)
            with allure.step(f'设定截图策略: before={ssBefore} , after={ssAfter}'):
                ssOption['before'] = ssBefore
                ssOption['after'] = ssAfter
                self.screenshotOption = ssOption

            with allure.step("启动浏览器"):
                self.__seleniumHelper = SeleniumHelper(browserType, options)
        if self.initSteps:
            with allure.step('--启动指令--'):
                for iStep in self.initSteps:
                    try:
                        self.runStep(iStep)
                    except Exception as e:
                        if self.exitIfSetupErr:
                            self.skipModuleReason = "Module Setup 失败"
                            # pytest.fail("Module Setup 失败")

    def runTitle(self, titleIdx: int):

        title = self.titleLst[titleIdx]

        severity: str = title.get('severity')
        if severity:
            allure.dynamic.severity(severity)

        feature = title.get('feature')
        if feature:
            allure.dynamic.feature(feature)
        story = title.get('story')
        if story:
            allure.dynamic.story(story)
        allure.dynamic.title(title['name'])
        if title.get('description'):
            allure.dynamic.description_html(title.get('description'))

        if title.get('tag'):
            allure.dynamic.tag(*title.get('tag'))

        if self.skipModuleReason:
            with allure.step(self.skipModuleReason):
                pytest.skip(self.skipModuleReason)
            return
        steps = title.get('steps')
        errSkip: int = title.get('errSkip', 0)
        try:
            for iStep in steps:
                self.runStep(iStep)
        except Exception as e:
            if errSkip == 0:
                pytest.fail(e.args[0])
                # raise e
            elif errSkip == 2:
                self.skipModuleReason = e.args[0]
                pytest.fail(e.args[0])
                # raise e

    def quit(self):
        self.__seleniumHelper.quit()

    def runStep(self, step: dict):
        item = list(step.items())[0]
        act = item[0]
        value: dict = item[1]
        seleniumHelper = self.__seleniumHelper
        stepName: str = value["step_name"]
        stepNameV = self._getSysStrValue(stepName, value, False)
        options: dict = value.get('_options', {})

        ssOption: dict = options.get('screenshot', self.screenshotOption)
        assertError: int = options.get('assertError', False)
        errSkip: int = options.get('errSkip', 0)

        def _runOneStep(step_name: str, currentRepeat: int, totalRepeat: int):

            # region init
            stepV: dict = deepcopy(value)
            for iKey in stepV.keys():
                if type(stepV[iKey]) == str:
                    stepV[iKey] = self._getSysStrValue(stepV[iKey], stepV, iKey != 'step_name')

            isSubStep = totalRepeat > 1
            ssBefore: int = ssOption.get('before', 0)
            ssAfter: int = ssOption.get('after', 2)
            beforeSS: bytes = None
            if isSubStep:
                step_name = step_name + f'\t{currentRepeat}/{totalRepeat}'

            def _saveScreenshot(beforeSS: bytes, before: bool, after: bool):
                if ssBefore != 0 and before:
                    allure.attach(beforeSS, "<Step Before>", attachment_type=allure.attachment_type.PNG)

                if after:
                    afterSS: bytes = seleniumHelper.BROSWER_screenshot()
                    allure.attach(afterSS, "<Step After>", attachment_type=allure.attachment_type.PNG)

            if assertError:
                step_name = step_name + '\t[E]'
            # endregion

            with allure.step(step_name):
                try:
                    if ssBefore != 0:
                        beforeSS: bytes = seleniumHelper.BROSWER_screenshot()

                    if act == EngineActIdEnum.sub_steps:
                        for iSubAction in stepV.get('actions'):
                            self.runStep(iSubAction)
                    elif act == EngineActIdEnum.SYS_sleep:
                        time.sleep(stepV.get('time'))
                        pass
                    elif act == EngineActIdEnum.ENGINE_setVar:
                        varName = stepV.get("name")
                        # varValue = self._getSysStrValue(stepV.get('value'))
                        varValue = stepV.get('value')
                        with allure.step(f'{varName} = {varValue}'):
                            self.__vars[varName] = varValue

                    elif act == EngineActIdEnum.BROWSER_getUrl:
                        seleniumHelper.BROWSER_getUrl(stepV.get('url'), stepV.get('force'))

                    elif act == EngineActIdEnum.BROWSER_switch_to_window:
                        seleniumHelper.BROWSER_switch_to_window(stepV.get('idx'))

                    elif act == EngineActIdEnum.BROWSER_wait:
                        self._waitBrowser(stepV)

                    elif act == EngineActIdEnum.BROWSER_screenshot:
                        png = seleniumHelper.BROSWER_screenshot()
                        allure.attach(png, "窗口截图", attachment_type=allure.attachment_type.PNG)

                    elif act == EngineActIdEnum.BROWSER_close:
                        seleniumHelper.BROWSER_close()

                    elif act == EngineActIdEnum.BROWSER_maximize_window:
                        seleniumHelper.BROWSER_maximize_window()

                    elif act == EngineActIdEnum.BROWSER_set_window_size:
                        seleniumHelper.BROWSER_set_window_size(stepV.get('width', 1280), stepV.get('height', 800))

                    elif act == EngineActIdEnum.BROWSER_SET_implicitly_wait:
                        seleniumHelper.BROWSER_SET_implicitly_wait(stepV.get('time'))

                    elif act == EngineActIdEnum.WEB_wait:
                        self._waitElem(stepV)

                    elif act == EngineActIdEnum.WEB_assertAttr:
                        elem = self.findElem(stepV)
                        elemValue: str = elem.get_attribute(stepV.get('attribute'))
                        pos = elemValue.find(stepV.get('value'))
                        with allure.step(f"：内容发现位置：{pos}"):
                            assert pos > -1

                    elif act == EngineActIdEnum.WEB_sendKeys:
                        elem = self.findElem(stepV)
                        v = stepV.get('value')
                        seleniumHelper.WEB_scrollIntoView(elem)
                        if stepV.get('clear'):
                            with allure.step('清空已有内容'):
                                elem.clear()
                        with allure.step(f"输入值 : {v}"):
                            elem.send_keys(v)

                    elif act == EngineActIdEnum.WEB_click:
                        elem = self.findElem(stepV)
                        seleniumHelper.WEB_scrollIntoView(elem)
                        with allure.step("单击元素"):
                            elem.click()

                    elif act == EngineActIdEnum.WEB_select:
                        selectBy: str = stepV.get('selectBy')
                        selectContent: str = stepV.get('selectContent')
                        elem = self.findElem(stepV)
                        with allure.step(f"点选Item by={selectBy} , content={selectContent}"):
                            seleniumHelper.WEB_select(elem, selectBy, selectContent)

                    elif act == EngineActIdEnum.WEB_switch_to_frame:
                        seleniumHelper.WEB_switch_to_frame(stepV.get('by'), stepV.get('path'))
                        pass

                    elif act == EngineActIdEnum.WEB_switch_to_default_content:
                        seleniumHelper.WEB_switch_to_default_content()
                        pass

                    else:
                        raise NotImplementedError()

                # except NotImplementedError as e:
                #     allure.attach(json.dump(value), "doc", attachment_type=allure.attachment_type.JSON)
                #     raise e
                except Exception as e:
                    if isinstance(e, NotImplementedError):
                        allure.attach(json.dump(stepV), "doc", attachment_type=allure.attachment_type.JSON)
                        raise e
                    if not assertError:
                        _saveScreenshot(beforeSS, ssBefore != 0, ssAfter != 0)
                        if errSkip == 0:
                            msg = ErrSkipMsgEnum.Msg100
                            with allure.step(msg):
                                # pytest.fail(str(e))
                                raise Exception(msg, err=e, step=stepV)
                        elif errSkip == 1:
                            msg = ErrSkipMsgEnum.Msg101
                            with allure.step(msg):
                                pass
                        else:
                            msg = ErrSkipMsgEnum.Msg102
                            raise Exception(msg, err=e, step=stepV)
                    else:
                        with allure.step(ErrSkipMsgEnum.Msg000):
                            pass
                else:

                    if assertError:
                        _saveScreenshot(beforeSS, ssBefore != 0, ssAfter != 0)
                        if errSkip == 0:
                            msg = ErrSkipMsgEnum.Msg010
                            with allure.step(msg):
                                raise Exception(msg, step=stepV)
                        elif errSkip == 1:
                            msg = ErrSkipMsgEnum.Msg011
                            with allure.step(msg):
                                pass
                        else:
                            msg = ErrSkipMsgEnum.Msg012
                            with allure.step(msg):
                                raise Exception(msg, step=stepV)

                    else:
                        _saveScreenshot(beforeSS, ssBefore == 1, ssAfter == 1)

        repeat = options.get('repeat', 1)

        if repeat > 1:
            iRepeat = 1
            # with allure.step(stepNameV + f'\t重复{repeat}次'):
            while iRepeat <= repeat:
                _runOneStep(stepNameV, iRepeat, repeat)
                iRepeat += 1
        else:
            _runOneStep(stepNameV, 1, 1)

    def findElem(self, act: dict) -> WebElement:
        by: str = act.get('by') or By.XPATH
        path: str = act.get('path')
        waitOptions: dict = act.get('waitOptions')
        if waitOptions:
            with allure.step(f"[等待]定位元素 : by={by} , path={path} , wait={waitOptions.get('method')}"):
                elem = self.__seleniumHelper.WEB_waitElem(waitOptions, by, path)
        else:
            with allure.step(f"定位元素 : by={by} , path={path}"):
                elem = self.__seleniumHelper.elemFind(by, path)
        return elem

    def _waitElem(self, act: dict) -> WebElement:
        by: str = act.get('by') or By.XPATH
        path: str = act.get('path')
        waitOptions: dict = act.get('waitOptions', {"timeout": 10, "until": "until", "frequency": 0.5, })

        elem = self.__seleniumHelper.WEB_waitElem(waitOptions, by, path)
        return elem

    def _waitBrowser(self, act: dict) -> WebElement:
        method: str = act.get("method")
        timeout: float = act.get("timeout") or 10.0
        until: bool = act.get('until', "until") == "until"
        frequency: float = act.get('frequency') or 0.5
        args: list = act.get('args', [])
        if method == 'current_url_changes':
            method = 'url_changes'
            args = [self.__seleniumHelper.BROWSER_current_url()]
        elem = self.__seleniumHelper.BROWSER_wait(method, timeout, frequency, until, args)
        return elem

    def _getFakeValue(self, old: str) -> str:
        def _fakerRepl(matched: re):
            funcName = matched.groups()[0]
            func = getattr(self.fake, funcName)
            argsStr = matched.groups()[1]
            result = None
            if argsStr:
                args = json.loads(argsStr)
                if type(args) == list:
                    result = func(*args)
                elif type(args) == dict:
                    result = func(**args)
            else:
                result = func()
            return str(result)

        return self.__class__.__fakerReg.sub(_fakerRepl, old)

    def _getSysStrValue(self, old: str, node: dict = None, fake: bool = True):
        result = old

        def _argSub(matched: re.Match):
            value = node.get(matched.groups()[0])
            return str(value)

        result = self.__class__.__argsReg.sub(_argSub, result)

        # 替换VARS
        def _varSub(matched: re.Match):
            return str(self.__vars.get(matched.groups()[0]))

        result = self.__class__.__varsReg.sub(_varSub, result)

        # 替换键盘Keys
        def _keysSub(matched: re.Match):
            return getattr(Keys, matched.groups()[0])

        result = self.__class__.__keysReg.sub(_keysSub, result)

        # 替换FAKER
        if fake:
            result = self._getFakeValue(result)
        return result
